package com.simpou.commons.utils.reflection;

import com.simpou.commons.utils.reflection.condition.PojoFieldCondition;
import com.simpou.commons.utils.string.Strings;
import com.simpou.commons.utils.validation.Assertions;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

/**
 * Métodos úteis para manipulações do anotações.
 *
 * @author Jonas Pereira
 * @since 2011-07-13
 * @version 2012-05-12
 */
public final class Annotations {

    /**
     * <p>isAnnotatedWith.</p>
     *
     * @param clasz Classe que deveria conter as anotações.
     * @param annotations Classes das anotações a serem verificadas.
     * @return Anotações não definidas na classe. Não retorna null.
     */
    public static List<Class<? extends Annotation>> isAnnotatedWith(
            final Class<?> clasz, final List<Class<? extends Annotation>> annotations) {
        List<Class<? extends Annotation>> notDefineds = new ArrayList<Class<? extends Annotation>>();

        for (Class<? extends Annotation> annotationClass : annotations) {
            if (!isAnnotatedWith(clasz, annotationClass)) {
                notDefineds.add(annotationClass);
            }
        }

        return notDefineds;
    }

    /**
     * <p>isAnnotatedWith.</p>
     *
     * @param clasz Classe que deveria conter as anotações.
     * @param annotation Classe das anotaçção a ser verificada.
     * @return a boolean.
     */
    public static boolean isAnnotatedWith(final Class<?> clasz,
            final Class<? extends Annotation> annotation) {
        return clasz.isAnnotationPresent(annotation);
    }

    /**
     * <p>isAnnotatedWith.</p>
     *
     * @param method Método que deveria conter as anotações.
     * @param annotation Classe das anotaçção a ser verificada.
     * @return a boolean.
     */
    public static boolean isAnnotatedWith(final Method method,
            final Class<? extends Annotation> annotation) {
        return method.getAnnotation(annotation) != null;
    }

    /**
     * Verifica se dois conjuntos de anotações apresentam pelo menos uma em
     * comum.
     *
     * @param annots1 Primeiro conjunto.
     * @param annots2 Segundo conjunto.
     * @return Se existe pelo menos uma anotação comum entre os dois grupos.
     */
    public static boolean matchOne(final List<Annotation> annots1,
            final List<Annotation> annots2) {
        for (Annotation annot1 : annots1) {
            for (Annotation annot2 : annots2) {
                if (isSameAnnotation(annot1, annot2)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Define se duas instâncias de anotações referem-se a uma mesma classe de
     * anotação. Valores não são considerados, para isto uso "equals".
     *
     * @param annot1 Primeira anotação.
     * @param annot2 Segunda anotação
     * @return Se anotações são instâncias de uma mesma classe.
     */
    public static boolean isSameAnnotation(final Annotation annot1,
            final Annotation annot2) {
        return annot1.getClass().getName().equals(annot2.getClass().getName());
    }

    /**
     * Define se uma anotação está presente em um conjunto.
     *
     * @param annots Conjunto de anotações.
     * @param annot Anotação a ser verificada.
     * @return Se conjunto contém anotação.
     */
    public static boolean containsAnnotation(
            final Collection<Annotation> annots, final Annotation annot) {
        for (Annotation annotIter : annots) {
            if (isSameAnnotation(annot, annotIter)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param field Campo.
     * @return true se o acesso jaxb é feito via campo, false se feito via método get
     */
    public static boolean isFieldAccessPriority(final Field field) {
        final Class<?> declaringClass = field.getDeclaringClass();
        return declaringClass.isAnnotationPresent(XmlAccessorType.class)
                && declaringClass.getAnnotation(XmlAccessorType.class).value().equals(XmlAccessType.FIELD);

    }

    /**
     * @param field Campo anotado.
     * @param annotClass Anotação a ser obtida.
     * @param fieldAccessPriority Define que, caso anotação esteja acesível
     * tanto via campo como por propriedade, a anotação via campo será
     * considerada.
     * @return Anotação de um campo. Busca no campo e no getter.
     */
    public static <T extends Annotation> T getPojoFieldAnnotation(final Field field, final Class<T> annotClass, final boolean fieldAccessPriority) {
        Assertions.check(field, new PojoFieldCondition());
        final T annotField;
        final T annotProp;

        annotField = field.getAnnotation(annotClass);
        {
            final Method method;
            try {
                method = Reflections.getGetterMethod(field);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
                assert false;
                return null;
            }
            annotProp = method.getAnnotation(annotClass);
        }
        final T annot;
        if (annotField == null) {
            if (annotProp == null) {
                annot = null;
            } else {
                annot = annotProp;
            }
        } else {
            if (annotProp == null) {
                annot = annotField;
            } else if (fieldAccessPriority) {
                annot = annotField;
            } else {
                annot = annotProp;
            }
        }
        return annot;
    }
}
